<template>
  <div class="file-tree">
    <div class="file-tool">
      <div class="file-label"> <i class="el-icon-files"></i>&nbsp;&nbsp;文件视图 </div>
      <div class="btn">
        <i class="el-icon-finished" :class="{ active: isMulti }" @click="toggleMulti()">多选</i>
        <el-popover
          placement="left-start"
          width="500"
          v-model="searchVisible"
          @show="getTreeList()"
        >
          <el-autocomplete
            style="width: 100%"
            class="inline-input"
            value-key="path"
            v-model="searchInput"
            :fetch-suggestions="querySearch"
            placeholder="请输入文件名"
            :trigger-on-focus="false"
            @select="handleSelect"
          />
          <template #reference>
            <Icon icon="material-symbols:manage-search" :size="20" title="搜索" />
          </template>
        </el-popover>
        <el-dropdown>
          <span class="el-dropdown-link">
            <Icon icon="material-symbols:more-horiz" :size="20" />
          </span>
          <template #dropdown>
            <el-dropdown-menu>
              <el-dropdown-item @click="toggleExpandTree(1)">全部展开</el-dropdown-item>
              <el-dropdown-item @click="toggleExpandTree(2)">全部收起</el-dropdown-item>
              <el-dropdown-item @click="toggleExpandTree(3)">展开选中</el-dropdown-item>
              <el-dropdown-item @click="toggleExpandTree(4)">收起其他</el-dropdown-item>
              <el-dropdown-item
                divided
                :disabled="
                  !(
                    (!isMulti && selectFiles.some((fs) => fs.type === 'directory')) ||
                    selectFiles.length < 1
                  )
                "
                @click="doAdd()"
                ><Icon icon="material-symbols:add" :size="16" />新增</el-dropdown-item
              >
              <el-dropdown-item @click="doEdit()" :disabled="!(!isMulti && selectFiles.length > 0)"
                ><Icon icon="material-symbols:edit" :size="16" />编辑</el-dropdown-item
              >
              <el-dropdown-item
                @click="doDelete()"
                :disabled="!(!isMulti && selectFiles.length > 0)"
                ><Icon icon="material-symbols:delete" :size="16" />删除</el-dropdown-item
              >
            </el-dropdown-menu>
          </template>
        </el-dropdown>
      </div>
    </div>
    <div class="tree-main" ref="treeMain">
      <el-dropdown
        placement="bottom-start"
        v-if="hoverRow"
        :style="downStyle"
        @visible-change="onTipHide"
      >
        <span class="el-dropdown-link tip-btn">
          <Icon icon="material-symbols:more-horiz" :size="20" />
        </span>
        <template #dropdown>
          <el-dropdown-menu>
            <el-dropdown-item :disabled="hoverRow.type !== 'directory'" @click="doAdd(hoverRow)"
              ><Icon icon="material-symbols:add" :size="16" />新增</el-dropdown-item
            >
            <el-dropdown-item @click="doEdit(hoverRow)"
              ><Icon icon="material-symbols:edit" :size="16" />编辑</el-dropdown-item
            >
            <el-dropdown-item @click="doDelete(hoverRow)"
              ><Icon icon="material-symbols:delete" :size="16" />删除</el-dropdown-item
            >
          </el-dropdown-menu>
        </template>
      </el-dropdown>
      <vxe-table
        resizable
        show-overflow
        :show-header="false"
        ref="xTree"
        size="small"
        height="auto"
        :column-config="{
          resizable: true
        }"
        :row-config="{
          keyField: 'path',
          useKey: true,
          height: 25,
          isHover: true
        }"
        :tree-config="{
          accordion: false,
          line: true,
          iconOpen: 'vxe-icon-square-minus',
          iconClose: 'vxe-icon-square-plus',
          expandAll: config.expandAll,
          rowField: 'path',
          children: 'nodes',
          reserve: true
        }"
        :data="treeData"
        :scroll-y="{ enabled: false }"
        @dragstart="onDragStart"
        @dragend="onDragEnd"
      >
        <vxe-column field="name" title="Name" tree-node>
          <template #default="{ row }">
            <div
              class="file-node"
              draggable="true"
              :class="{
                active: selectFiles.find((file) => file.path === row.path),
                'drag-in':
                  dragOverStatus?.overNodeKey === row.path &&
                  `${dragOverStatus?.dropPosition}` === '0',
                'drag-up':
                  dragOverStatus?.overNodeKey === row.path &&
                  `${dragOverStatus?.dropPosition}` === '-1',
                'drag-down':
                  dragOverStatus?.overNodeKey === row.path &&
                  `${dragOverStatus?.dropPosition}` === '1'
              }"
              :path="row.path"
              @click="openFile(row)"
              @dragover="onDragOver"
              @dragenter="onDragEnter"
              @dragleave="onDragLeave"
              @drop="onDrop"
            >
              <template v-if="row.type === 'directory'">
                <i
                  class="tree-node-icon"
                  :class="
                    $refs.xTree.isTreeExpandByRow(row) ? 'vxe-icon-folder-open' : 'vxe-icon-folder'
                  "
                ></i>
              </template>
              <template v-else>
                <i class="tree-node-icon vxe-icon-file-txt"></i>
              </template>
              <span class="file-name">{{ row.name }}</span>
            </div>
            <div
              class="node-tip"
              @mouseenter="(e) => onTipHover(e, row, 'enter')"
              @mouseleave="(e) => onTipHover(e, row, 'leave')"
            >
              <span class="el-dropdown-link">
                <Icon icon="material-symbols:more-horiz" :size="20" />
              </span>
            </div>
          </template>
        </vxe-column>
      </vxe-table>
    </div>
  </div>
</template>

<script>
import { getFsTree, addFile, renameFile, deleteFile, moveFile } from '@/api/file'
import DragData from '../utils/DragData'

export default {
  props: {
    config: {
      type: Object,
      default: () => ({})
    }
  },
  data() {
    return {
      treeData: [], // 树形结构数据
      treeList: [], // 拉平树数据（用于搜索）
      selectFiles: [], // 选择的目录和文件
      isMulti: false, // 是否多选模式
      expandedList: [], //展开的节点list
      searchVisible: false, //文件搜索弹框
      searchInput: '', // 文件搜索变量
      savedScrollTop: 0, // 树滚动条位置
      delayedDragEnterLogic: null, // 拖拽延迟
      dragNodeHighlight: false, //拖拽元素是否高亮
      // 拖拽对象树形
      dragOverStatus: {
        overNodeKey: '',
        dropPosition: '',
        dragNode: {},
        dropNode: {}
      },
      hoverRow: null,
      downStyle: {}
    }
  },
  created() {
    this.getTree()
  },
  methods: {
    // 树搜索过滤
    querySearch(queryString, cb) {
      var restaurants = this.treeList
      var results = queryString
        ? restaurants.filter((node) => node.name.includes(queryString))
        : restaurants
      // 调用 callback 返回建议列表的数据
      cb(results)
    },
    // 搜索文件选择
    handleSelect(item) {
      const node = this.$refs.xTree.getRowById(item.path)
      let pnode = this.$refs.xTree.getParentRow(node)
      let expandList = [node]
      while (pnode) {
        expandList.push(pnode)
        pnode = this.$refs.xTree.getParentRow(pnode)
      }
      this.$refs.xTree.setTreeExpand(expandList, true)
      this.searchVisible = false
      this.searchInput = ''
      this.openFile(node)
    },
    // 查询树数据
    async getTree(node, updateList, opType) {
      const res = await getFsTree({
        path: '/data/'
      })
      this.treeData = res.fs_tree
      this.$nextTick(() => {
        if (this.selectFiles.length > 0) {
          const fnode = this.selectFiles[0]
          // 定位滚动位置
          const row = this.$refs.xTree.getRowById(fnode.path)
          this.$refs.xTree.scrollToRow(row)
        }
        if (node) {
          // 右侧重新打开
          const newNode = this.$refs.xTree.getRowById(node.path)
          if (node.type === 'directory') {
            this.$refs.xTree.setTreeExpand([node], true)
          }
          this.openFile(newNode)
        } else if (updateList && Array.isArray(updateList)) {
          const addFiles = updateList
            .filter((path) => !this.selectFiles.some((fs) => fs.path === path))
            .map((path) => this.$refs.xTree.getRowById(path))
          this.selectFiles.push(...addFiles)
          if (opType === 'delete') {
            this.selectFiles = this.selectFiles.filter((s) => s.type === 'directory')
          }
          this.$emit(
            'openFile',
            JSON.parse(JSON.stringify(this.selectFiles)),
            this.selectFiles.find((n) => n.type === 'directory')
              ? true
              : this.selectFiles.length > 1
          )
        }
      })
    },
    // 将树拉平放入list
    getTreeList() {
      const result = []
      const eachFn = function (nodes) {
        for (let i = 0; i < nodes.length; i++) {
          result.push(nodes[i])
          if (nodes[i].nodes) {
            eachFn(nodes[i].nodes)
          }
        }
      }
      eachFn(this.treeData)
      this.treeList = result
    },
    // 打开文件
    openFile(node) {
      // 多选模式
      if (this.isMulti) {
        // 是否已选择
        const findIndex = this.selectFiles.findIndex((file) => file.path === node.path)
        if (findIndex > -1) {
          // 取消文件的选择
          this.selectFiles.splice(findIndex, 1)
          // 取消整个文件夹的选择
          if (node.type === 'directory') {
            for (let i = 0; i < node.nodes.length; i++) {
              const cIndex = this.selectFiles.findIndex((file) => file.path === node.nodes[i].path)
              this.selectFiles.splice(cIndex, 1)
            }
          }
        } else {
          // 添加选择
          if (node.type === 'directory') {
            // 选择整个文件夹和文件夹下的文件
            this.selectFiles.push(
              node,
              ...(node.nodes ? node.nodes.filter((n) => n.type != 'directory') : [])
            )
          } else {
            // 选择文件和文件的父文件夹
            const cNode = this.$refs.xTree.getRowById(node.path)
            const pNode = this.$refs.xTree.getParentRow(cNode)
            const pIndex = this.selectFiles.findIndex((file) => file.path === pNode.path)
            if (pIndex < 0) {
              this.selectFiles.push(pNode)
            }
            this.selectFiles.push(node)
          }
        }
        this.$emit('openFile', JSON.parse(JSON.stringify(this.selectFiles)), true)
      } else if (node.type === 'directory') {
        // 如果选择的是文件夹，就是多选处理
        this.selectFiles = [
          node,
          ...(node.nodes ? node.nodes.filter((n) => n.type != 'directory') : [])
        ]
        this.$emit('openFile', JSON.parse(JSON.stringify(this.selectFiles)), true)
      } else {
        this.selectFiles = [node]
        this.$emit('openFile', JSON.parse(JSON.stringify(this.selectFiles)), false)
      }
    },
    // 切换多选模式
    toggleMulti() {
      this.isMulti = !this.isMulti
      this.selectFiles = []
      this.$emit('openFile', this.selectFiles)
    },
    //获取元素到文档顶部和左边的距离
    getOffset(ele) {
      if (!ele.getClientRects().length) {
        return { top: 0, left: 0 }
      }
      var rect = ele.getBoundingClientRect()
      if (rect.width || rect.height) {
        var doc = ele.ownerDocument
        var win = doc.defaultView
        var docElem = doc.documentElement
        return {
          //元素距离视窗顶部距离，滚动高度，元素边框厚度
          top: rect.top + win.pageYOffset - docElem.clientTop,
          left: rect.left + win.pageXOffset - docElem.clientLeft
        }
      }
      return rect
    },
    //计算拖拽节点的放置方式0（作为目标节点的子节点），-1（放置在目标节点的前面）,1（放置在目标节点的后面）
    calDropPosition(e) {
      var offsetTop = this.getOffset(e.target).top
      var offsetHeight = e.target.offsetHeight
      var pageY = e.pageY
      var gapHeight = 0.2 * offsetHeight
      if (pageY > offsetTop + offsetHeight - gapHeight) {
        //放在目标节点后面-同级
        return 1
      }
      if (pageY < offsetTop + gapHeight) {
        //放在目标节点前面-同级
        return -1
      }
      //放在目标节点里面-作为子节点
      return 0
    },
    //拖拽开始
    onDragStart(e) {
      e.stopPropagation()
      this.dragNodeHighlight = true
      e.dataTransfer.effectAllowed = 'move'
      const path = e.srcElement.getAttribute('path')
      const node = this.$refs.xTree.getRowById(path)
      e.dataTransfer.setData('nodeData', node)
      DragData.setData('nodeData', node)
      let pnode = this.$refs.xTree.getParentRow(node)
      this.dragOverStatus.dragNode = {
        nodeData: node,
        parentNode: pnode
      }
    },

    //进入目标节点
    onDragEnter(e) {
      e.preventDefault()
      e.stopPropagation()
      const path = this.getDomPath(e.srcElement)
      const nodeData = DragData.getData('nodeData')
      this.dragOverStatus.dragNode = {
        nodeData,
        parentNode: null
      }
      //拖拽节点与目标节点是同一个，return掉
      if (path === this.dragOverStatus.dragNode.nodeData.path) {
        return
      }
      //设置dragEnter定时器，停留250毫秒后触发事件
      if (!this.delayedDragEnterLogic) {
        this.delayedDragEnterLogic = {}
      }
      Object.keys(this.delayedDragEnterLogic).forEach((key) => {
        clearTimeout(this.delayedDragEnterLogic[key])
      })
      this.delayedDragEnterLogic[path] = setTimeout(() => {
        this.dragOverStatus.overNodeKey = path //当前经过的可放置的节点的key
      }, 250)
    },

    onDragOver(e) {
      e.preventDefault()
      e.stopPropagation()
      const path = this.getDomPath(e.srcElement)
      if (this.dragOverStatus.overNodeKey === path) {
        this.dragOverStatus.dropPosition = this.calDropPosition(e) //放置标识0，-1,1
      }
      // this.dragOverClass = this.setDragOverClass();
    },

    onDragLeave(e) {
      e.stopPropagation()
      // this.dragOverClass = "";
    },

    onDrop(e) {
      e.preventDefault()
      e.stopPropagation()
      let target = e.srcElement
      if (!target.getAttribute('path')) {
        target = target.parentNode
      }
      const path = this.getDomPath(e.srcElement)
      // this.dragOverClass = "";
      //拖拽节点与目标节点是同一个，不做任何操作
      if (!path || this.dragOverStatus.dragNode.nodeData.path === path) {
        this.resetDragDrop()
        return
      }
      const node = this.$refs.xTree.getRowById(path)
      // 不可放在文件里
      if (node && this.dragOverStatus.dropPosition === 0 && node.type != 'directory') {
        this.resetDragDrop()
        return
      }
      let pnode = this.$refs.xTree.getParentRow(node)
      this.getDropData(this.dragOverStatus, {
        nodeData: node,
        parentNode: pnode
      })
      this.resetDragDrop()
    },

    onDragEnd(e) {
      e.stopPropagation()
      e.preventDefault()
      this.resetDragDrop()
    },

    resetDragDrop() {
      this.dragNodeHighlight = false
      this.dragOverStatus.dragNode = null
      this.dragOverStatus.overNodeKey = ''
      this.dragOverStatus.dropPosition = ''
      DragData.clear()
    },

    getDomPath(dom) {
      if (!dom) {
        return ''
      }
      let target = dom
      let cnt = 0
      while (target && !target.getAttribute('path') && cnt < 3) {
        target = target.parentNode
        cnt += 1
      }
      return target.getAttribute('path')
    },

    async getDropData(drag, drop) {
      var dragData = drag.dragNode.nodeData
      // var dragParent = drag.dragNode.parentNode;
      var dropData = drop.nodeData
      // var dropParent = drop.dropNode.parentNode;
      var dropPosition = drag.dropPosition //0作为子级，-1放在目标节点前面，1放在目标节点后面

      //把拖拽元素从父节点中删除
      // const oldParent = this.$refs.xTree.getParentRow(dragData);
      // const newParent = this.$refs.xTree.getParentRow(dropData);
      // console.log(oldParent, newParent);
      await moveFile({
        old_path: dragData.path,
        new_path: dropData.path,
        drag_type: dropPosition
      })
      this.getTree(this.selectFiles[0])
    },
    // 新增文件夹（未选择父节点，直接在根节点添加）
    doAdd(item) {
      const row = item || this.selectFiles.find((fs) => fs.type === 'directory')
      this.$prompt('请输入文件名', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        // eslint-disable-next-line no-useless-escape
        inputPattern: /^[^\s\\/:\*\?\"<>\|]+$/,
        inputErrorMessage: '请输入正确的文件名',
        closeOnClickModal: false
      })
        .then(async ({ value }) => {
          await addFile({
            path: (row ? row.path : '/data') + '/' + value
          })
          this.getTree()
        })
        .catch(() => {})
    },
    // 重命名选择的文件
    doEdit(item) {
      const row = item || this.selectFiles[0]
      this.$prompt('请输入新的文件名', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        inputValue: row.name,
        // eslint-disable-next-line no-useless-escape
        inputPattern: /^[^\s\\/:\*\?\"<>\|]+$/,
        inputErrorMessage: '请输入正确的文件名',
        closeOnClickModal: false
      })
        .then(async ({ value }) => {
          const new_path = row.path.replace(`/${row.name}`, `/${value}`)
          await renameFile({
            old_path: row.path,
            new_path: new_path
          })
          row.path = new_path
          row.name = value
          this.getTree(row)
        })
        .catch(() => {})
    },
    // 删除文件
    doDelete(item) {
      const row = item || this.selectFiles[0]
      this.$confirm(`确认删除文件${row.type === 'directory' ? '夹' : ''} ${row.name}`, '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning',
        closeOnClickModal: false
      })
        .then(async () => {
          await deleteFile({
            path: row.path
          })
          this.getTree()
          this.selectFiles = []
          this.$emit(
            'openFile',
            JSON.parse(JSON.stringify(this.selectFiles)),
            this.selectFiles.find((n) => n.type === 'directory')
              ? true
              : this.selectFiles.length > 1
          )
        })
        .catch(() => {})
    },
    // 树操作
    toggleExpandTree(type) {
      // 展开所有
      if (type === 1) {
        this.$refs.xTree.setAllTreeExpand(true)
      } else if (type === 2) {
        this.$refs.xTree.setAllTreeExpand(false)
      } else if (type === 3) {
        this.$refs.xTree.setTreeExpand(this.selectFiles, true)
      } else if (type === 4) {
        this.$refs.xTree.setAllTreeExpand(false)
        const expandList = [...this.selectFiles]
        // 找到所有父节点
        this.selectFiles.forEach((node) => {
          let pNode = this.$refs.xTree.getParentRow(node)
          while (pNode && !expandList.find((fs) => fs.path === pNode.path)) {
            expandList.push(pNode)
            pNode = this.$refs.xTree.getParentRow(pNode)
          }
        })
        this.$refs.xTree.setTreeExpand(expandList, true)
      }
    },
    onTipHover(e, row, type) {
      if (type === 'leave') {
        return
      }
      this.hoverRow = row
      const mainRect = this.$refs.treeMain.getBoundingClientRect()
      const targetRect = e.target.getBoundingClientRect()
      this.downStyle = {
        position: 'absolute',
        'z-index': 999,
        left: `${targetRect.x - mainRect.x}px`,
        top: `${targetRect.y - mainRect.y}px`
      }
    },
    onTipHide(show) {
      if (!show) {
        this.hoverRow = null
        this.downStyle = {
          position: 'absolute',
          'z-index': 0,
          left: `-999px`
        }
      }
    }
  }
}
</script>

<style lang="less" scoped>
.file-tree {
  display: flex;
  flex-direction: column;
  height: 100%;
  width: 100%;

  .file-tool {
    background-color: var(--el-color-primary);
    flex: 0 0 25px;
    color: #fff;
    font-size: 14px;
    padding: 0px 10px;
    display: flex;
    align-items: center;
    justify-content: space-between;

    .btn {
      display: flex;
      align-items: center;
      i {
        cursor: pointer;
        margin-left: 8px;
        color: #fff;

        &.active {
          background: var(--el-color-primary);
          border: 1px solid;
          border-radius: 2px;
          font-size: 12px;
          padding: 1px 2px;
        }
      }
    }
  }

  .tree-main {
    position: relative;
    flex: 1;
    font-size: 14px;
    overflow: hidden;
  }

  .tree-node-icon {
    width: 16px;
    text-align: center;
  }

  :deep(.vxe-icon-square-minus, .vxe-icon-square-plus) {
    background-color: #ffffff;
  }

  .node-tip {
    position: absolute;
    right: 0;
    top: 0;
    display: inline-block;
    cursor: pointer;
    background: #fff;
    height: 20px;
    visibility: hidden;

    transition: visibility 0s linear 0.3s;

    &:hover {
      visibility: visible;
    }
  }

  :deep(.file-node) {
    position: relative;
    cursor: pointer;

    &.drag-in {
      background: #73b8fa;
    }
    &.drag-up {
      border-top: 2px solid #73b8fa;
    }
    &.drag-down {
      border-bottom: 2px solid #73b8fa;
    }

    &:hover {
      .edit-btn {
        display: inline-block;
      }

      & + .node-tip {
        visibility: visible;
      }
    }

    .file-name {
      padding: 2px;
    }

    &.active {
      .file-name {
        background: var(--el-color-primary);
        color: #ffffff;
      }
    }

    .edit-btn {
      display: none;
      padding: 3px;
      background: #fff;
      border-radius: 2px;
      position: absolute;
      top: 0;
      right: 0;
      font-size: 14px;
      color: #dd3333;

      i {
        margin-right: 5px;
        &:last-child {
          margin-right: 0;
        }
      }

      i:hover {
        color: red;
      }
    }
  }
}
</style>
